home *** CD-ROM | disk | FTP | other *** search
/ PC World Komputer 2010 April / PCWorld0410.iso / pluginy Firefox / 7661 / 7661.xpi / components / RILlist.js < prev    next >
Text File  |  2009-12-24  |  43KB  |  1,209 lines

  1. Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
  2.  
  3.  
  4. function RILlist() {
  5.     this.wrappedJSObject = this;
  6.     
  7.     this.batch = [];
  8.     this.iByUrl = [];
  9.     this.iByItemId = [];
  10. }
  11.  
  12. RILlist.prototype = {
  13.  
  14.     // properties required for XPCOM registration:
  15.     classDescription: "Read It Later List Javascript XPCOM Component",
  16.     classID:          Components.ID("{6449ffe0-aab4-11de-8a39-0800200c9a66}"),
  17.     contractID:       "@ril.ideashower.com/rillist;1",
  18.     
  19.     QueryInterface: XPCOMUtils.generateQI(),    
  20.     
  21.     //////////////////////////////////////////////
  22.     
  23.     init : function()
  24.     {
  25.         this.APP    = Components.classes['@ril.ideashower.com/rildelegate;1'].getService().wrappedJSObject;
  26.         
  27.         
  28.         this.fetchThread.RILmainThreadHandle.prototype = {
  29.                     
  30.             run: function() {
  31.                 try {
  32.                     
  33.                     let LIST = Components.classes['@ril.ideashower.com/rillist;1'].getService().wrappedJSObject;
  34.                     
  35.                     for(let i in this.temp) {
  36.                         LIST[i] = this.temp[i];
  37.                     }
  38.                     
  39.                     LIST.tagIndexNeedsRebuild = true;
  40.                     LIST.fetchCompleted(this.temp.status);
  41.                     
  42.                 } catch(err) {
  43.                     Components.utils.reportError(err);
  44.                 }
  45.             },
  46.             
  47.             QueryInterface: function(iid) {
  48.                 if (iid.equals(Components.interfaces.nsIRunnable) ||
  49.                     iid.equals(Components.interfaces.nsISupports)) {
  50.                     return this;
  51.                 }
  52.                 throw Components.results.NS_ERROR_NO_INTERFACE;
  53.             }
  54.             
  55.         }
  56.         
  57.     },
  58.     
  59.     ///////////////////////////////////////////////
  60.     
  61.     
  62.     // Loads a copy of the list into memory
  63.     fetch : function() {
  64.  
  65.         if (this.fetching) return false;
  66.         
  67.         this.fetching = true;
  68.         
  69.         // TODO Should we do a flushBatch before a fetch??
  70.                 
  71.         // Start thread
  72.         let thread = Components.classes["@mozilla.org/thread-manager;1"].getService().newThread(0);
  73.         thread.dispatch(this.fetchThread, thread.DISPATCH_NORMAL);
  74.            
  75.     },
  76.     
  77.     fetchCompleted : function(status) {        
  78.         this.fetching = false;
  79.         this.tagIndexNeedsRebuild = true;
  80.         
  81.         if (!status)
  82.         {
  83.             this.APP.listError = true;    
  84.         } else {
  85.             this.APP.updateUnreadCount();            
  86.         }
  87.         
  88.         this.APP.listHasBeenReloaded();        
  89.     },
  90.     
  91.     fetchThread : {
  92.         
  93.         run : function()
  94.         {
  95.             try {
  96.                 let sql, statement, row;
  97.                 this.list = [];                
  98.                 this.iByUrl = {};
  99.                 this.iByItemId = {};
  100.                 this.tags = [];
  101.                 
  102.                 this.temp = { //used in the main thread to know what items to copy over
  103.                     list : this.list,
  104.                     iByUrl : this.iByUrl,
  105.                     iByItemId : this.iByItemId,
  106.                     tags : this.tags
  107.                 }
  108.                               
  109.                 // Connect to delegate
  110.                 this.APP = Components.classes['@ril.ideashower.com/rildelegate;1'].getService().wrappedJSObject;
  111.                 
  112.                 
  113.                 
  114.                 // Get uniqueId
  115.                 
  116.                 sql = "SELECT unique_id FROM vars";
  117.                 statement = this.APP.DB.createStatement(sql);
  118.                 try {
  119.                     statement.executeStep();
  120.                     this.temp.uniqueId = statement.row.unique_id;
  121.                     statement.reset();
  122.                 }
  123.                 finally {
  124.                     statement.reset();
  125.                 }
  126.                 
  127.                                 
  128.                 // Get list
  129.                 
  130.                 sql = "SELECT item_id, unique_id, url, title, time_updated, offline_web, offline_text FROM items";
  131.                 statement = this.APP.DB.createStatement(sql);
  132.                 let strTime, timeUpdated;
  133.                 try {
  134.                     while (statement.step())
  135.                     {
  136.                         
  137.                         row = statement.row;
  138.                         
  139.                         // milisecond time fix (caused by <= 2.0.1)
  140.                         strTime = row.time_updated + '';
  141.                         if (strTime.length > 10)
  142.                             timeUpdated = strTime.substr(0,10) * 1;
  143.                         else
  144.                             timeUpdated = row.time_updated;
  145.                         
  146.                         this.addNewItemToMemoryList.call(this,{
  147.                             itemId      : row.item_id,
  148.                             uniqueId    : row.unique_id,
  149.                             url         : row.url,
  150.                             title       : row.title,
  151.                             timeUpdated : timeUpdated,
  152.                             offlineWeb  : row.offline_web,
  153.                             offlineText : row.offline_text
  154.                         }, this.temp);
  155.                     } 
  156.                 }
  157.                 finally {
  158.                     statement.reset();
  159.                 }
  160.                 
  161.                 // Get tags
  162.                 
  163.                 let item, itemId, tag;
  164.                 sql = "SELECT item_id, tag FROM tags";
  165.                 statement = this.APP.DB.createStatement(sql);
  166.                 try {
  167.                     while (statement.step())
  168.                     {
  169.                         
  170.                         row = statement.row;
  171.                         itemId = row.item_id;
  172.                         item = this.temp.list[ this.temp.iByItemId[itemId] ];
  173.                         
  174.                         if (item)
  175.                         {
  176.                             tag = row.tag;
  177.                             
  178.                             if (!item.tags)     item.tags = [];
  179.                             if (!item.tagList)  item.tagList = '';
  180.                             
  181.                             item.tags.push( tag );
  182.                             item.tagList += (item.tagList.length > 0 ? ', ' : '' ) + tag;
  183.                             
  184.                             // Add to list of all tags
  185.                             //this.temp.tags.push( {n:1, tag:tag} );
  186.                         }
  187.                     } 
  188.                 }
  189.                 finally {
  190.                     statement.reset();
  191.                 }
  192.                 
  193.                 
  194.                 // Get scroll positions
  195.                 
  196.                 sql = "SELECT item_id, view, section, page, node_index, percent, time_updated FROM scroll ORDER BY time_updated";
  197.                 statement = this.APP.DB.createStatement(sql);
  198.                 try {
  199.                     while (statement.step())
  200.                     {
  201.                         row = statement.row;
  202.                         itemId = row.item_id;
  203.                         item = this.temp.list[ this.temp.iByItemId[itemId] ];
  204.                         
  205.                         if (item)
  206.                         { 
  207.                             if (!item.scroll) item.scroll = {};
  208.                             
  209.                             item.scroll[ row.view ] = {
  210.                                 view:       row.view,
  211.                                 section:    row.section,
  212.                                 page:       row.page,
  213.                                 nodeIndex:  row.node_index,
  214.                                 percent:    row.percent
  215.                             }
  216.                             item.percent = row.percent; // use the latest scroll position
  217.                         }
  218.                     } 
  219.                 }
  220.                 finally {
  221.                     statement.reset();
  222.                 }
  223.                 
  224.                 
  225.                 // Get resolver
  226.                 let i;
  227.                 this.temp.resolver = [];
  228.                 sql = "SELECT item_id, url FROM resolver";
  229.                 statement = this.APP.DB.createStatement(sql);
  230.                 try {                    
  231.                     while (statement.step())
  232.                     {                        
  233.                         row = statement.row;
  234.                         
  235.                         i = this.temp.iByItemId[ row.item_id ];
  236.                         if (i >= 0) // make sure item still exists //TODO - clean up entries that don't anymore?
  237.                         {
  238.                             this.temp.iByUrl[ this.APP.parseUrl(row.url, true) ] = i;
  239.                         
  240.                             // add to a resolver object that we can use when rebuilding the iByUrl index
  241.                             this.temp.resolver.push( {itemId:row.item_id, url:row.url} );
  242.                         }
  243.                     } 
  244.                 }
  245.                 finally {
  246.                     statement.reset();
  247.                 }
  248.                 
  249.                 // Report back to main thread
  250.                 this.temp.status = true;
  251.                 this.reportToMainThread();
  252.                 
  253.                 
  254.                 // -- Continue on with some extra tasks that the user does not need to wait on
  255.                 
  256.                 // Clean empty assets
  257.                 let batchStatement;
  258.                 this.cleanUpBatch = [];
  259.                 sql = "SELECT assets.asset_domain AS asset_domain, COUNT(assets_items.item_id) AS retain FROM assets LEFT OUTER JOIN assets_items ON assets.asset_domain = assets_items.asset_domain GROUP BY assets.asset_domain";
  260.                 statement = this.APP.DB.createStatement(sql);
  261.                 try {                    
  262.                     while (statement.step())
  263.                     {
  264.                         row = statement.row;                        
  265.                         if (row.retain == 0) {
  266.                             this.APP.ASSETS.removeAssetDomain(row.asset_domain);
  267.                             batchStatement = this.APP.DB.createStatement("DELETE FROM assets WHERE asset_domain = :assetDomain");
  268.                             batchStatement.params.assetDomain = row.asset_domain;
  269.                             this.cleanUpBatch.push(batchStatement);
  270.                         }
  271.                     } 
  272.                 }
  273.                 finally {
  274.                     statement.reset();
  275.                 }
  276.                 
  277.                 if (this.cleanUpBatch.length > 0) this.APP.DB.executeAsync( this.cleanUpBatch , this.cleanUpBatch.length, null );
  278.                 
  279.                 
  280.             } catch (e) {
  281.                 Components.utils.reportError(e);
  282.                 this.temp.status = false;
  283.                 this.reportToMainThread();
  284.             }
  285.         },
  286.     
  287.         addNewItemToMemoryList : function(item) {
  288.                        
  289.             let i = this.list.length;
  290.             
  291.             this.list[ i ] = item;
  292.             
  293.             // If it has a tmp unique id (pre-sync or local list only)
  294.             if (!item.itemId) item.itemId = item.uniqueId * -1;
  295.             
  296.             this.iByUrl[ this.APP.parseUrl(item.url, true) ] = i;
  297.             this.iByItemId[ item.itemId ] = i;
  298.             
  299.             if (item.itemId < this.lastTmpId) lastTmpId = item.itemId;
  300.             
  301.             return i;
  302.         
  303.         },
  304.         
  305.         reportToMainThread : function() {
  306.             let main = Components.classes["@mozilla.org/thread-manager;1"].getService().mainThread;
  307.             main.dispatch(new this.RILmainThreadHandle(this.temp), this.DISPATCH_NORMAL);            
  308.         },
  309.         
  310.         QueryInterface: function(iid) {
  311.             if (iid.equals(Components.interfaces.nsIRunnable) ||
  312.                 iid.equals(Components.interfaces.nsISupports)) {
  313.                     return this;
  314.             }
  315.             throw Components.results.NS_ERROR_NO_INTERFACE;
  316.         },
  317.         
  318.         RILmainThreadHandle : function(temp) {
  319.             this.temp = temp;
  320.         } // prototype defined above in RILlist.init
  321.         
  322.     },
  323.     
  324.     
  325.     // List Lookup
  326.         
  327.     itemById : function(item_id) {
  328.         let i = this.iByItemId[item_id];
  329.         if (i >= 0) return this.list[i];
  330.     },
  331.     
  332.     itemByUrl : function(url) {
  333.         if (!url) return;
  334.         let parsedUrl = this.APP.parseUrl(url, true);
  335.         let i = this.iByUrl[parsedUrl];
  336.         if (i >= 0) return this.list[i]; 
  337.     },
  338.     
  339.     rebuildIindex : function() {
  340.         // TODO: Optimize this
  341.         
  342.         this.APP.d('rebuildIindex');
  343.         
  344.         // -- Main list -- //
  345.         let i, n;
  346.         
  347.         this.iByUrl = {};
  348.         this.iByItemId = {};
  349.         
  350.         // Make a copy of the list
  351.         let old = this.list.slice();
  352.         this.list = [];
  353.         
  354.         // Go through old list to rebuild list and indexes
  355.         let newI=0;
  356.         for(i in old) {
  357.             
  358.             if (old[i])
  359.             {          
  360.                 this.list[ newI ] = old[i];   
  361.                 this.iByUrl[ this.APP.parseUrl( this.list[newI].url, true ) ] = newI;
  362.                 this.iByItemId[ this.list[newI].itemId ] = newI;
  363.                 newI++;
  364.             }
  365.         }
  366.         
  367.         // Go through resolver and add entries
  368.         for(n in this.resolver)
  369.         {  
  370.             i = this.iByItemId[ this.resolver[n].itemId ];
  371.             this.iByUrl[ this.APP.parseUrl( this.resolver[n].url, true ) ] = i;
  372.         }
  373.         
  374.         // -- Update unread count -- //
  375.         this.APP.updateUnreadCount();
  376.         
  377.     },
  378.     
  379.     // -- Current List -- //
  380.     
  381.     getCurrentList : function() {
  382.         if (!this.currentList || this.currentListNeedsRefresh) {
  383.             this.currentList = this.list.filter(this.currentFilter).sort(this.sortCurrent);
  384.             this.currentListNeedsRefresh = false;
  385.         }
  386.     },
  387.     
  388.     currentFilter : function(element, index, array) {
  389.         return (element.percent > 0);
  390.     },
  391.     
  392.     sortCurrent : function(a,b) {
  393.         return a.percent < b.percent ? 1 : (a.percent > b.percent ? -1 : 0);
  394.     },
  395.     
  396.     
  397.     // -- Tags -- //
  398.     
  399.     // Cycles through reading list and builds a tag->item lookup that can be used as a list for populating a list view
  400.     rebuildTagIndex : function()
  401.     {
  402.         if (this.tagIndexNeedsRebuild)
  403.         {
  404.             let index = {};
  405.             let tagList = [];
  406.             let tempIndex = {};
  407.             let i, ti, t, tag, tags;
  408.             
  409.             for(i=0; i<this.list.length; i++)
  410.             {
  411.                 tags = this.list[i].tags;
  412.                 if (tags)
  413.                 {
  414.                     for(t=0; t<tags.length; t++)
  415.                     {
  416.                         tag = tags[t];
  417.                         if (!tag) continue;
  418.                         
  419.                         // tag to item index
  420.                         if (!index[tag])
  421.                         {
  422.                             index[tag] = [];
  423.                         }
  424.                         index[tag].push( this.list[i] );
  425.                         
  426.                         // tag list
  427.                         if (tempIndex[tag] >= 0)
  428.                         {
  429.                             tagList[ tempIndex[tag] ].n++;
  430.                         }
  431.                         else
  432.                         {
  433.                             tagList.push( {n:1,tag:tag} );
  434.                             tempIndex[ tag ] = tagList.length-1;
  435.                             this.APP.d( tagList.length-1 + ' = ' + tag );
  436.                         }
  437.                         
  438.                     }
  439.                 }
  440.             }
  441.             
  442.             // Build most used tags list
  443.             this.topTags = tagList.slice();
  444.             this.topTags.sort( this.sortTagsByCount );            
  445.             
  446.             // Sort array by tag name
  447.             this.tags = tagList;
  448.             this.tags.sort( this.sortTagsByName );
  449.             
  450.             this.tagItemIndex = index;
  451.             this.tagToTagIndex = false;
  452.             this.tagIndexNeedsRebuild = false;
  453.             
  454.         }
  455.     },
  456.     
  457.     sortTagsByCount : function(a, b)
  458.     {        
  459.         return a.n > b.n ? -1 : a.n == b.n ? 0 : 1;   
  460.     },
  461.     
  462.     sortTagsByName : function(a, b)
  463.     {        
  464.         return a.tag > b.tag ? 1 : a.tag == b.tag ? 0 : -1;   
  465.     },
  466.     
  467.     tagByTag : function(tag)
  468.     {
  469.         this.rebuildTagIndex();
  470.         if (!this.tagToTagIndex)
  471.         {
  472.             let tagToTagIndex = {};
  473.             let i;
  474.             for(i=0; i<this.tags.length; i++)
  475.             {
  476.                 tagToTagIndex[ this.tags[i].tag ] = i;
  477.             }
  478.             this.tagToTagIndex = tagToTagIndex;
  479.         }
  480.         
  481.         return this.tags[ this.tagToTagIndex[tag] ];
  482.     },
  483.     
  484.     
  485.     // -- Updates -- //
  486.     
  487.     // -- AddtoMemoryList is in the fetch Thread object -- //
  488.     
  489.     removeFromMemoryList : function(itemId, noRebuild) {
  490.         
  491.         this.list[ this.iByItemId[itemId] ] = false;
  492.         
  493.         // rebuild iByUrl and iByItemId indexes, is there a better way?  Seems wasteful
  494.         if (!noRebuild) this.rebuildIindex();
  495.         
  496.     },
  497.  
  498.     add : function(item, batch, noSync, noAutoDownload)
  499.     {
  500.         try {
  501.             
  502.         
  503.         // -- Reasons not to save
  504.         // Empty
  505.         if (!item.url) return false;
  506.         
  507.         // Already in List
  508.         if (this.itemByUrl(item.url)) return false; //already exists in list
  509.         
  510.         // Not a valid link PARSER
  511.         // TODO use isValidUrl()
  512.         let parsed = this.APP.parseUri(item.url);
  513.     if (parsed.protocol != 'http' && parsed.protocol != 'https') return false;
  514.     
  515.         
  516.         // Temp passing around
  517.         // This is done because when item is added to memory list, it would set.tagList = new value, so when saveTags is called below and it
  518.         // checks the new tags verus existing, they would match, and therefore not be saved.
  519.         let tagList = item.tagList;
  520.         delete item.tagList;
  521.         // positions may have the same issue as tags
  522.         let positions = item.positions;
  523.         delete item.positions;
  524.         
  525.         
  526.         // -- Saving
  527.         if (!item.uniqueId)                     item.uniqueId = this.nextUniqueId();
  528.         if (!item.itemId)                       item.uniqueId * - 1; //TODO this needs to be *= ?? is this causing problems?
  529.         if (!item.timeUpdated)                  item.timeUpdated = this.APP.now();
  530.         if (!item.offlineWeb)                   item.offlineWeb = 0;        
  531.         if (!item.offlineText)                  item.offlineText = 0;       
  532.         if (!item.percent)                      item.percent = 0;
  533.         //this.APP.d( this.APP.ar(item, true))
  534.         
  535.         // Update Memory List
  536.         this.fetchThread.addNewItemToMemoryList.call(this,item);
  537.         
  538.         // Save to database
  539.         let statement = this.APP.DB.createStatement("INSERT INTO items (item_id,unique_id,url,title,time_updated,offline_web,offline_text,percent) VALUES (:itemId,:uniqueId,:url,:title,:timeUpdated,:offlineWeb,:offlineText,:percent) ");
  540.         statement.params.itemId         = item.itemId;
  541.         statement.params.uniqueId       = item.uniqueId;
  542.         statement.params.url            = item.url;
  543.         statement.params.title          = item.title;
  544.         statement.params.timeUpdated    = item.timeUpdated;
  545.         statement.params.offlineWeb     = item.offlineWeb;
  546.         statement.params.offlineText    = item.offlineText;
  547.         statement.params.percent        = item.percent;
  548.  
  549.         this.batch.push( statement );
  550.         
  551.         // Tags
  552.         if (item.itemId && tagList)
  553.             this.saveTags(item.itemId, tagList, true, noSync);
  554.             
  555.         // Scroll Positions
  556.         if (item.itemId && positions)
  557.             this.updateScrollPositions(item.itemId, positions, true, noSync);
  558.         
  559.         if (!noSync)
  560.         {            
  561.             // keep track of new item ids in this batch
  562.             if (!this.APP.SYNC.syncBatchItems) this.APP.SYNC.syncBatchItems = {new:{}};
  563.             this.APP.SYNC.syncBatchItems.new[item.itemId] = true;
  564.             
  565.             // add to queue
  566.             this.APP.SYNC.addToSyncQueue('new', item.url, true);
  567.         }
  568.         
  569.         if (!batch) {
  570.             this.endBatchAndRefresh();
  571.         }
  572.         
  573.         return item.itemId;
  574.     
  575.         } catch (e) {
  576.             Components.utils.reportError(e);
  577.         }
  578.         
  579.     },
  580.     
  581.     mark : function(itemId, batch, noSync, deleteIt) {
  582.         let item = this.itemById(itemId);
  583.         if (!item) return;
  584.         let url = item.url; // needs this for sync queue
  585.         
  586.         // Remove item from memory list
  587.         if (item.tagList && item.tagList.length > 0)
  588.             this.tagIndexNeedsRebuild = true;
  589.         
  590.         this.removeFromMemoryList(itemId, true); // do not rebuild index because it will be rebuilt when batch is cleared
  591.         this.readListNeedsRefresh();
  592.         this.currentListNeedsRefresh = true;
  593.         
  594.         // Save change to database
  595.         
  596.         // remove item entry
  597.         let statement = this.APP.DB.createStatement("DELETE FROM items WHERE item_id = :itemId");
  598.         statement.params.itemId = itemId;        
  599.         this.batch.push( statement );
  600.         
  601.         // remove tags entries
  602.         statement = this.APP.DB.createStatement("DELETE FROM tags WHERE item_id = :itemId");
  603.         statement.params.itemId = itemId;        
  604.         this.batch.push( statement );
  605.         
  606.         // remove scroll entries
  607.         
  608.         // remove resolver entries
  609.         statement = this.APP.DB.createStatement("DELETE FROM resolver WHERE item_id = :itemId");
  610.         statement.params.itemId = itemId;        
  611.         this.batch.push( statement );
  612.         
  613.         // remove assets retain
  614.         statement = this.APP.DB.createStatement("DELETE FROM assets_items WHERE item_id = :itemId");
  615.         statement.params.itemId = itemId;        
  616.         this.batch.push( statement );
  617.         
  618.                
  619.         if (!noSync) {
  620.             this.APP.SYNC.addToSyncQueue( deleteIt ? 'delete' : 'read', url, true);
  621.         }        
  622.         
  623.         if (!batch) {
  624.             this.endBatchAndRefresh();
  625.         }
  626.         
  627.         // remove offline directory
  628.         this.APP.ASSETS.removeFolderForItemId( itemId );
  629.     },
  630.     
  631.     saveTitle : function(itemId, title, batch, noSync, syncWait) {               
  632.         
  633.         let item = this.itemById(itemId);
  634.         if (item.title != title)
  635.         {
  636.             // Update Memory List
  637.             item.title = title;
  638.             
  639.             // Save Change to Database
  640.             let statement = this.APP.DB.createStatement("UPDATE items SET title = :title WHERE item_id = :itemId");
  641.             statement.params.title = title;
  642.             statement.params.itemId = itemId;
  643.             
  644.             this.batch.push( statement );
  645.             
  646.             if (!noSync) {
  647.                 this.APP.SYNC.addToSyncQueue('title', item.url, true);
  648.             }
  649.             
  650.             if (!batch) {
  651.                 this.flushBatch();
  652.             }
  653.         }
  654.     },
  655.     
  656.     saveTags : function(itemId, tagList, batch, noSync, syncWait) { 
  657.     
  658.         this.APP.d('{}--');
  659.         let item = this.itemById(itemId);
  660.         
  661.         if (!item) return false;
  662.  
  663.         if (this.APP.trim(item.tagList?item.tagList:'') != this.APP.trim(tagList))
  664.         {
  665.             let statement, tag;
  666.             let tags = tagList.split(/,\s*?/);
  667.                                     
  668.             // Clear all tags for item
  669.             item.tags = [];
  670.             item.tagList = '';
  671.             statement = this.APP.DB.createStatement("DELETE FROM tags WHERE item_id = :itemId");
  672.             statement.params.itemId = itemId;
  673.             this.batch.push( statement );
  674.             
  675.             // Create new tags
  676.             for(let i in tags)
  677.             {                
  678.                 tag = this.APP.trim(tags[i]);
  679.                 if (!tag || tag.length == 0) continue;
  680.                 
  681.                 // Update Memory list
  682.                 item.tags.push( tag );
  683.                 this.tags.push( {tag:tag,n:1} ); // This being used anymore?
  684.                 item.tagList += (item.tagList.length > 0 ? ', ' : '' ) + tag;
  685.                 this.tagIndexNeedsRebuild = true;
  686.                 
  687.                 // Save Change to Database
  688.                 statement = this.APP.DB.createStatement("REPLACE INTO tags (item_id, tag) VALUES (:itemId,:tag)");
  689.                 statement.params.itemId = itemId;
  690.                 statement.params.tag = tag;
  691.                 
  692.                 this.batch.push( statement );
  693.             }
  694.                         
  695.             if (!noSync) {
  696.                 this.APP.SYNC.addToSyncQueue('tags', item.url, true);
  697.             }
  698.             
  699.             if (!batch) {
  700.                 this.flushBatch();
  701.             }
  702.         }
  703.     },
  704.     
  705.     renameTag : function(tag, newTag, batch, noSync)
  706.     {
  707.         if (!newTag || newTag.length == 0 || tag == newTag) return;
  708.         
  709.     this.rebuildTagIndex();
  710.         
  711.         
  712.         let i, item, reg, newReg, statement;
  713.         for(i in this.tagItemIndex[tag])
  714.         {            
  715.             reg = new RegExp('(^|,)(\\s*)?'+this.APP.regexSafe(tag)+'(\\s*)?(,|$)', 'i');
  716.             newReg = new RegExp('(^|,)(\\s*)?'+this.APP.regexSafe(newTag)+'(\\s*)?(,|$)', 'i');
  717.             
  718.             // Update in memory list
  719.             item = this.tagItemIndex[tag][i];
  720.             if (item.tagList.match(newReg))
  721.             {
  722.                 delete item.tags[ item.tags.indexOf(tag) ];
  723.                 item.tagList = item.tagList.replace( reg, '$4' );   
  724.         
  725.                 // Save change to database - remove old tag, the new one already exists on this item      
  726.                 statement = this.APP.DB.createStatement("DELETE FROM tags WHERE item_id = :itemId AND tag = :oldTag");
  727.                 statement.params.itemId = item.itemId;
  728.                 statement.params.oldTag = tag;
  729.                 this.batch.push( statement );              
  730.             }
  731.             else
  732.             {
  733.                 item.tags[ item.tags.indexOf(tag) ] = newTag;
  734.                 item.tagList = item.tagList.replace( reg, '$1$2' + newTag + '$3$4' );  
  735.         
  736.                 // Save change to database - replace this tag with the new one      
  737.                 statement = this.APP.DB.createStatement("UPDATE tags SET tag = :newTag WHERE  item_id = :itemId AND tag = :oldTag");
  738.                 statement.params.newTag = newTag;
  739.                 statement.params.itemId = item.itemId;
  740.                 statement.params.oldTag = tag;
  741.                 this.batch.push( statement );               
  742.             }
  743.             this.tagIndexNeedsRebuild = true;
  744.         }
  745.         
  746.         //Sync
  747.         if (!noSync) {
  748.             this.APP.SYNC.addToSyncQueue('tags', item.url, true);
  749.         }
  750.         
  751.         if (!batch) {
  752.             this.flushBatch();
  753.         }
  754.         
  755.     },
  756.     
  757.     removeTag : function(tag, batch, noSync)
  758.     {
  759.         if (!tag || tag.length == 0) return;
  760.         
  761.     this.rebuildTagIndex();        
  762.         
  763.         let i, item, reg, newReg, existsInItem, statement;
  764.         for(i in this.tagItemIndex[tag])
  765.         {            
  766.             reg = new RegExp('(^|,)(\\s*)?'+this.APP.regexSafe(tag)+'(\\s*)?(,|$)', 'i');
  767.             
  768.             // Update in memory list
  769.             item = this.tagItemIndex[tag][i];
  770.             delete item.tags[ item.tags.indexOf(tag) ];
  771.             item.tagList = item.tagList.replace( reg, '$4' );
  772.             this.tagIndexNeedsRebuild = true;
  773.         }
  774.         
  775.         // Save change to database          
  776.         statement = this.APP.DB.createStatement("DELETE FROM tags WHERE tag = :tag");
  777.         statement.params.tag = tag;
  778.         this.batch.push( statement ); 
  779.                 
  780.         //Sync
  781.         if (!noSync) {
  782.             this.APP.SYNC.addToSyncQueue('tags', item.url, true);
  783.         }
  784.         
  785.         if (!batch) {
  786.             this.flushBatch();
  787.         } 
  788.         
  789.     },
  790.     
  791.     compareAndUpdateTags : function(itemId, newTags, oldTags, batch)
  792.     {
  793.         if (!newTags) return;
  794.         let newList;
  795.         let oldList;
  796.         
  797.         // Just need to find out if the tag lists are different.  Each list may be in a completely different order
  798.         let updateTags = false;
  799.         
  800.         // Quick checks
  801.         // - compare number of items (if they are equal, we still need to keep checking)
  802.         if (!oldTags) updateTags = true;
  803.         
  804.         if (!updateTags)
  805.         {
  806.             newList = newTags.split(/\s*?,\s*?/);
  807.             oldList = oldTags.split(/\s*?,\s*?/);
  808.             if (newList.length != oldList.length) updateTags = true;
  809.         }
  810.         
  811.         
  812.         // Longer checks
  813.         // - loop through newTags and look in oldTags for each (stop when one is not found)
  814.         if (!updateTags && newList)
  815.         {
  816.             let i;
  817.             for(i=newList.length-1; i>=0; i--) // go backwards assuming newer tags will more likely be at the end
  818.             {
  819.                 if (newList[i] && !oldTags.match(newList[i]))
  820.                 {
  821.                     updateTags = true;
  822.                     break;
  823.                 }
  824.             }
  825.         }
  826.         
  827.         if (updateTags)
  828.             this.saveTags(itemId, newTags, batch, true);
  829.         
  830.     },
  831.     
  832.     changeURL : function(itemId, newUrl, batch, noSync) {               
  833.         //TODO see mod of this in app, if/else's are wrong
  834.         let item = this.itemById(itemId);
  835.         let oldUrl = item.url;
  836.         if (oldUrl != newUrl)
  837.         {   
  838.      
  839.             // Check to see if the newURL already exists in the list
  840.             //    if it does, we need to delete the old entry and add the oldURL as a resolve to the new one
  841.             //  This syncs a delete to the server for the old entry if it's not in the send queue
  842.             
  843.             //  if it does not exist, we need to update the old item to use the newURL
  844.             //  This syncs a delete to the server for the old entry if it's not in the send queue
  845.             //  This syncs a new to the server for the old entry if it's not in the send queue
  846.                 
  847.             let newItem = this.itemByUrl(newUrl);
  848.             if (newItem && newItem.itemId != itemId)
  849.             {
  850.                 // if the new url already exists in the list, delete the old (duplicate) entry but still add old url to resolver
  851.                 this.mark(itemId, true, true, true); // we'll handle the sync part of the deletion in a moment
  852.                 
  853.                 // Update Memory Resolver            
  854.                 this.addUrlToResolverForItemId(newItem.itemId, oldUrl);
  855.                 this.rebuildIindex();
  856.             }
  857.             
  858.             else
  859.             {
  860.                 // Update Memory List
  861.                 item.url = newUrl;
  862.             
  863.                 // Save Change to Database
  864.                 let statement = this.APP.DB.createStatement("UPDATE items SET url = :url WHERE item_id = :itemId");
  865.                 statement.params.url = newUrl;
  866.                 statement.params.itemId = itemId;
  867.             
  868.                 this.batch.push( statement );
  869.                 
  870.                 // Update Memory Resolver            
  871.                 this.addUrlToResolverForItemId(itemId, oldUrl, false, true);
  872.                 this.rebuildIindex();
  873.             }
  874.             
  875.             if (!noSync)
  876.             {
  877.                 // only need to sync the change if a 'new' entry doesn't already exist for this item, otherwise
  878.                 // it will already send the correct url when send occurs
  879.                 if (!this.APP.SYNC.syncBatchItems || !this.APP.SYNC.syncBatchItems.new[itemId])
  880.                 {                    
  881.                     // sync a delete for the old item
  882.                     this.APP.SYNC.addToSyncQueue( 'delete', oldUrl, true);
  883.                     
  884.                     // sync update as a new item
  885.                     this.APP.SYNC.addToSyncQueue('new', newUrl, true);
  886.                 }
  887.             }
  888.          
  889.             if (!batch)
  890.             {
  891.                 this.endBatchAndRefresh();
  892.             }
  893.         }
  894.     },
  895.     
  896.     updateTimeUpdated : function(itemId, timeUpdated, batch) {
  897.         
  898.         let item = this.itemById(itemId);
  899.  
  900.         // Update Memory List
  901.         item.timeUpdated = timeUpdated;
  902.         
  903.         // Save Change to Database
  904.         let statement = this.APP.DB.createStatement("UPDATE items SET time_updated = :timeUpdated WHERE item_id = :itemId");
  905.         statement.params.timeUpdated = timeUpdated;
  906.         statement.params.itemId = itemId;
  907.         
  908.         this.batch.push( statement );
  909.         
  910.         if (!batch) {
  911.             this.flushBatch();
  912.         }
  913.         
  914.     },
  915.     
  916.     updateItemId : function(itemId, newItemId, batch) {
  917.         /* is this ness??
  918.         let item = this.itemById(itemId);
  919.  
  920.         // Update Memory List
  921.         item.itemId = newItemId;
  922.         
  923.         // Save Change to Database
  924.         let statement = RIL.DB.createStatement("UPDATE items SET item_id = :newItemId WHERE item_id = :itemId");
  925.         statement.params.newItemId = newItemId;
  926.         statement.params.itemId = itemId;
  927.         
  928.         // Update other database tables
  929.         
  930.         // Update offline files
  931.         
  932.         // Update bookmark GUID?
  933.         
  934.         this.batch.push( statement );
  935.         
  936.         if (!batch) {
  937.             this.flushBatch();
  938.         }
  939.         */
  940.     },
  941.     
  942.     resetOffline : function()
  943.     {
  944.         // Update local list
  945.         let i;
  946.         for(i in this.list)
  947.         {
  948.             this.list[i].offlineWeb = 0;
  949.             this.list[i].offlineText = 0;
  950.         }
  951.         
  952.         // Update database
  953.         let statement = this.APP.DB.createStatement("UPDATE items SET offline_web = 0,  offline_text = 0");
  954.         this.batch.push( statement );
  955.         statement = this.APP.DB.createStatement("DELETE FROM assets");
  956.         this.batch.push( statement );
  957.         statement = this.APP.DB.createStatement("DELETE FROM assets_items");
  958.         this.batch.push( statement );
  959.         this.flushBatch();
  960.     },
  961.     
  962.     updateOffline : function(itemId, type, setting, retainDomains, batch)
  963.     {
  964.         
  965.         let statement;
  966.         let item = this.itemById(itemId);
  967.         
  968.         if (type == 2)
  969.         {
  970.             // Update Memory List            
  971.             item.offlineWeb = setting;
  972.             
  973.             // Save change to DB (statement)
  974.             statement = this.APP.DB.createStatement("UPDATE items SET offline_web = :setting WHERE item_id = :itemId");
  975.             
  976.         }
  977.         else if (type == 1)
  978.         {
  979.             // Update Memory List            
  980.             item.offlineText = setting;
  981.             
  982.             // Save change to DB (statement)
  983.             statement = this.APP.DB.createStatement("UPDATE items SET offline_text = :setting WHERE item_id = :itemId");
  984.             
  985.         }
  986.         
  987.         // Save Change to Database
  988.         statement.params.setting = setting;
  989.         statement.params.itemId = itemId;
  990.         this.batch.push( statement );
  991.         
  992.         // Update asset retain database
  993.         let retainDomain;
  994.         for(let i in retainDomains)
  995.         {
  996.             retainDomain = retainDomains[i];
  997.             
  998.             statement = this.APP.DB.createStatement("REPLACE INTO assets (asset_domain) VALUES (:assetDomain)");
  999.             statement.params.assetDomain = retainDomain;
  1000.             this.batch.push( statement );
  1001.             
  1002.             statement = this.APP.DB.createStatement("REPLACE INTO assets_items (item_id, asset_domain) VALUES (:itemId, :assetDomain)");
  1003.             statement.params.itemId = item.itemId;
  1004.             statement.params.assetDomain = retainDomain;
  1005.             this.batch.push( statement );
  1006.         }        
  1007.         
  1008.         if (!batch) {
  1009.             this.flushBatch();
  1010.         }
  1011.         
  1012.     },
  1013.     
  1014.     updateScrollPositions : function(itemId, positions, batch, noSync) {
  1015.     {
  1016.         if (!itemId || !positions) return;
  1017.         
  1018.         let position, p;
  1019.         for(p in positions)
  1020.         {
  1021.             position = positions[p];
  1022.             if (position && position.view)
  1023.                 this.updateScrollPosition(itemId,
  1024.                                             position.view,
  1025.                                             position.section,
  1026.                                             position.page,
  1027.                                             position.nodeIndex,
  1028.                                             position.percent,
  1029.                                             position.time_updated,
  1030.                                             batch,
  1031.                                             noSync);
  1032.             }
  1033.         }
  1034.     },
  1035.     
  1036.     updateScrollPosition : function(itemId, view, section, page, nodeIndex, percent, timeUpdated, batch, noSync, delay) {
  1037.         
  1038.         // Update Memory List
  1039.         let item = this.itemById(itemId);
  1040.         if (!item) return;
  1041.         
  1042.         if (!item.scroll) item.scroll = {};
  1043.         
  1044.         // check if scroll position is different than current
  1045.         let oldPosition = item.scroll[view];        
  1046.         if (oldPosition)
  1047.         {
  1048.             if (oldPosition.nodeIndex == nodeIndex) return false; // same, no need to do anything
  1049.         }
  1050.         
  1051.         item.scroll[view] = {
  1052.             view: view,
  1053.             section: section ? section : 0,
  1054.             page: page,
  1055.             nodeIndex: nodeIndex,
  1056.             percent: percent,
  1057.             timeUpdated: timeUpdated ? timeUpdated : this.APP.now()
  1058.         }
  1059.         
  1060.         item.percent = percent;
  1061.         this.currentListNeedsRefresh = true;
  1062.         
  1063.         
  1064.         // Update database        
  1065.         statement = this.APP.DB.createStatement("REPLACE INTO scroll (item_id, view, section, page, node_index, percent, time_updated) VALUES (:itemId, :view, :section, :page, :nodeIndex, :percent, :timeUpdated)");
  1066.         statement.params.itemId = itemId;
  1067.         statement.params.view = view;
  1068.         statement.params.section = section ? section : 0;
  1069.         statement.params.page = page;
  1070.         statement.params.nodeIndex = nodeIndex;
  1071.         statement.params.percent = percent;
  1072.         statement.params.timeUpdated = item.scroll[view].timeUpdated;
  1073.         this.batch.push( statement );
  1074.         
  1075.         
  1076.         //Sync
  1077.         if (!noSync) {
  1078.             this.APP.SYNC.addToSyncQueue('scroll', item.url, true, delay);
  1079.         }
  1080.         
  1081.         
  1082.         if (!batch) {
  1083.             this.flushBatch();
  1084.         }
  1085.         
  1086.         
  1087.     },
  1088.     
  1089.     // -- //
  1090.     
  1091.     flushScrollPositions : function() {
  1092.         
  1093.         let positions = this.pendingScrollPositions;
  1094.         
  1095.         let position;
  1096.         for(let i in positions) {
  1097.             position = positions[i];
  1098.             this.updateScrollPosition(position.itemId, position.view, position.section, 1, position.nodeIndex, Math.ceil( position.percent < 1 ? 0 : ( position.percent > 100 ? 100 : position.percent) ), null, true, false, true);
  1099.         }
  1100.         
  1101.         this.flushBatch();
  1102.         this.APP.refreshListInAllOpenWindows('current');
  1103.         
  1104.         this.pendingScrollPositions = {};
  1105.         
  1106.     },
  1107.     
  1108.     readListNeedsRefresh : function()
  1109.     {        
  1110.         this.APP.commandInAllOpenWindows('RIL', 'setReadListNeedsRefresh');
  1111.     },
  1112.     
  1113.     
  1114.     // -- //
  1115.     
  1116.     endBatchAndRefresh : function() {
  1117.         this.flushBatch();
  1118.         this.rebuildIindex();
  1119.             
  1120.         // Refresh display - if list is open, refresh it
  1121.         this.APP.refreshListInAllOpenWindows();
  1122.     },
  1123.     
  1124.     addToBatchAndFlush : function( statement ) {
  1125.         this.batch.push( statement );
  1126.     this.flushBatch();
  1127.     },
  1128.     
  1129.     flushBatch : function(callback) {        
  1130.         
  1131.     // grab a snapshot of the batch and then clear it
  1132.     let batch = this.batch.slice();
  1133.     this.batch = [];
  1134.         
  1135.         // Flush changes to DB
  1136.         if (this.uniqueIdNeedsFlush)
  1137.         {
  1138.            let statement = this.APP.DB.createStatement("UPDATE vars SET unique_id = :uniqueId");
  1139.            statement.params.uniqueId = this.uniqueId; 
  1140.            batch.push( statement );
  1141.         }
  1142.         if (batch.length > 0) {            
  1143.             this.APP.DB.executeAsync( batch , batch.length, callback?callback:this.genericResultHandler );
  1144.         }
  1145.         
  1146.         // Flush changes to sync batch if they exist
  1147.         this.APP.SYNC.flushBatch();        
  1148.     },
  1149.     
  1150.     genericResultHandler : {
  1151.         
  1152.         empty: true,
  1153.         
  1154.         handleResult : function(aResultSet) { this.empty = false; },
  1155.         
  1156.         handleError : function(aError) { Components.utils.reportError(aError.message) },
  1157.         
  1158.         handleCompletion : function(aReason) { } 
  1159.         
  1160.     },
  1161.     
  1162.     
  1163.     // -- Link Resolver -- //
  1164.     
  1165.     addUrlToResolverForItemId : function(itemId, url, doNotSaveToDB, batch)
  1166.     {
  1167.         if (!itemId || !url) return;
  1168.         
  1169.         if (!doNotSaveToDB)
  1170.         {
  1171.             // Add original to resolver
  1172.             let statement = this.APP.DB.createStatement("REPLACE INTO resolver (item_id, url) VALUES (:itemId, :url)");
  1173.             statement.params.itemId = itemId;
  1174.             statement.params.url = url;  
  1175.             
  1176.             if (!batch)
  1177.                 this.addToBatchAndFlush( statement );
  1178.             else
  1179.                 this.batch.push(statement);
  1180.         }
  1181.         
  1182.     
  1183.     // Add to resolver in memory
  1184.     let i = this.iByItemId[ itemId ];
  1185.         if (i) this.iByUrl[ this.APP.parseUrl(url), true ] = i;
  1186.         this.resolver.push( {itemId:itemId, url:url} );
  1187.     },
  1188.     
  1189.     
  1190.     
  1191.     // -- Helpers -- //
  1192.         
  1193.     nextUniqueId : function() {
  1194.         this.uniqueIdNeedsFlush = true;
  1195.         this.uniqueId++;
  1196.         return this.uniqueId;
  1197.     }
  1198.     
  1199.     
  1200.     
  1201. };
  1202.  
  1203.  
  1204.  
  1205. var components = [RILlist];
  1206. function NSGetModule(compMgr, fileSpec) {
  1207.   return XPCOMUtils.generateModule(components);
  1208. }
  1209.